我想整合不同的方法來使用 Observable
、 Signal
和 HttpClient
在 Angular 中檢索資料。根據我的觀察,我發現了六種資料檢索模式,每種模式都有其優點和缺點。在這篇文章分析了 Angular 中的資料檢索之後,我希望讀者能夠選擇自己喜歡的選擇並將其應用到他們的 Angular 專案中。
此示範將 signal 傳遞給 Pokemon API 以檢索 Pikachu 並使用不同的模式來顯示結果。資料檢索模式是:
npm i -save-exact ngxtension
import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideHttpClient } from "@angular/common/http";
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideExperimentalZonelessChangeDetection()
]
}
為示範提供 HttpClient
和 experimental zoneless。
import { appConfig } from './app.config';
bootstrapApplication(App, appConfig);
將 App
元件和 appConfig
引導到應用程式。
// pokemon.interface.ts
export interface Pokemon {
id: number;
name: string;
sprites: {
front_shiny: string
};
}
export interface DisplayPokemon {
id: number;
name: string;
img: string;
}
```typscript
```typescript
import { HttpClient } from "@angular/common/http";
import { inject } from "@angular/core";
import { map, Observable } from "rxjs";
import { DisplayPokemon, Pokemon } from "./pokemon.interface";
export const getPokemonFn = (): (id: number) => Observable<DisplayPokemon> => {
const httpClient = inject(HttpClient);
return (id: number) => {
return httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)
.pipe(
map((p) => ({
id: p.id,
name: p.name,
img: p.sprites.front_shiny
}))
);
}
}
getPokemonFn
函數是一個高階函數,它會傳回一個檢索 Pokemon 的函數。匿名函數接受 Pokemon Id 並呼叫伺服器來檢索 DisplayPokemon
Observable。
import { TitleCasePipe } from "@angular/common";
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
import { DisplayPokemon } from "../pokemon.interface";
@Component({
selector: 'app-pokemon',
standalone: true,
imports: [TitleCasePipe],
template: `
<div class="outer">
<div>
<img [attr.alt]="pokemon().name" [src]="pokemon().img" />
</div>
<p>
<span style="font-weight: bold;">Id:</span> <span>{{ pokemon().id }}</span>
<span style="font-weight: bold;">Name:</span>
<span>{{ pokemon().name | titlecase }}</span>
</p>
</div>
`,
})
export class PokemonComponent {
pokemon = input.required<DisplayPokemon>();
}
PokemonComponent
元件包含所需的訊號輸入 (signal input) pokemon
。它呈現皮卡丘的圖像、id 和名稱。
import { ChangeDetectionStrategy, Component, HostAttributeToken, inject, input } from "@angular/core";
import { DisplayPokemon } from "../pokemon.interface";
import { PokemonComponent } from "../app-pokemon/app-pokemon.compont";
@Component({
selector: 'app-pokemon-container',
standalone: true,
imports: [PokemonComponent],
template: `
<h3 style="padding: 0.5rem;">{{ pattern }}</h3>
@if (pokemon(); as pokemon) {
<app-pokemon [pokemon]="pokemon" />
}
`,
})
export class PokemonContainerComponent {
pokemon = input.required<DisplayPokemon | null | undefined>();
pattern = inject(new HostAttributeToken('pattern'), { optional: true }) || 'Signal Default Value';
}
PokemonContainerComponent
元件顯示模式名稱並呈現 PokemonComponent
元件。
現在,我可以一一描述每種模式。
<app-pokemon-container [pokemon]="pokemon$ | async" pattern="Observable + AsyncPipe + HttpClient" />
export class App {
id = signal(25);
getPokemon = getPokemonFn();
// old way. RxJS way before there is Signal
pokemon$ = this.getPokemon(this.id());
}
id
signal 的值為 25。範本使用 AsyncPipie
解析 pokemon$
並將結果指派給 AppPokemonContainerComponent
元件的 pokemon
input。
我的拙見:開發人員可以使用 RxJS
和 HttpClient
來檢索 Observable
並在範本中解析它以顯示結果。 如果我們想避免 AsyncPipe
和 Observable
,我們可以嘗試其他模式。
<app-pokemon-container [pokemon]="pokemon2()" pattern="Effect + HttpClient" />
pokemon2 = signal<DisplayPokemon | undefined>(undefined);
constructor() {
effect((OnCleanUp) => {
const subscription = this.getPokemon(this.id())
.subscribe((p) => this.pokemon2.set(p))
OnCleanUp(() => subscription.unsubscribe());
});
}
我聲明了一個 pokemon2
訊號 (signal) 並提供了一個 undefined
的初始值。 此 effect
追蹤 id
訊號 (signal) 並在訊號值 (signal value) 變化時執行邏輯。 effect
邏輯呼叫 API 來擷取 pokemon、訂閱 Observable 並覆寫 pokemon2
訊號 (signal) 的值。在 OnCleanUp
回呼函數中,在銷毀 effect
之前取消訂閱 (subscription)。
我的拙見:Angular 團隊領導和幾位專家建議盡量減少該 effect
的使用。此 effect
在每個查詢中建立一個新的 Observable
和 subscription
。此外,開發人員必須使用 OnCleanUp
回呼函數來清理 subscription
以避免 memory leaks。開發人員很容易忘記取消 subscription
。 該 effect
適用於高級工程師,他們可以做出正確的決定來使用 effect
或替代方案。
<app-pokemon-container [pokemon]="pokemon3()" pattern="HttpClient + toObservable + toSignal + SwitchMap" />
createToObservable(pokemonId: Signal<number>) {
return toObservable(pokemonId).pipe(
tap((id) => console.log(id)),
switchMap((id) => this.getPokemon(id))
);
}
pokemon3$ = this.createToObservable(this.id);
pokemon3 = toSignal<DisplayPokemon>(this.pokemon3$, { initialValue: undefined });
toObservable(this.id)
建立一個 Observable
並將 id
傳送給 switchMap
運算子以取得 pokemon。然後,toSignal
從 Observable
建立一個 signal
並將結果分配給 pokemon3
。然後,範本 pokemon3
的 signal函數來顯示資料。
我的拙見:這種模式很好,因為 HttpClient
總是完成並取消訂閱 (subscription)。此外,switchMap
在發出新請求之前會取消先前的請求。然而,toSignal
和 toObservable
在元件上加入了樣板程式碼,當這種模式重複出現時,元件將變得難以維護。當 root service 建立一個不取消訂閱 (subscription) 的 Observable
時,toSignal
可能會導致 memory leaks。工程師應該在元件中使用 toSignal
,這樣當元件被銷毀時,Observable
也會被銷毀。
<app-pokemon-container [pokemon]="pokemon4()" pattern="derivedAsync" />
pokemon4 = derivedAsync(() => this.getPokemon(this.id()));
使用 ngxtension
的 derivedAsync
函數來檢索 Pokemon。derivedAsync
的回傳類型是 Signal<T> | undefined
。
我的拙見:實用函數支援 Promise
和 Observable
,並回傳 Signal<T> | undefined
。函數使用Subject
發出一個值,並在 DestroyRef
的回呼中執行清理。預設行為是 switchAll
,它會取消先前的請求。它具有早期模式的所有優點,而沒有缺點。 Angular 開發人員不必擔心 AsyncPipe
、需要清理的訂閱 (subscription) 以及 toSignal(toObservable(signal))
樣板程式碼。
<app-pokemon-container [pokemon]="pokemon5()" pattern="derivedAsync + requireSync" />
pokemon5$ = this.getPokemon(this.id()).pipe(startWith(DEFAULT_POKEMON));
pokemon5 = derivedAsync(() => this.pokemon5$,
{
requireSync: true,
});
此模式也使用 ngxtension
的 derivedAsync
函數來檢索 Pokemon。 requiredSync
為 true 確保 Observable
在訂閱時發出一個值;因此,傳回類型為 Signal<T>
。
我的拙見:透過在 startWith
運算子中指定初始值,derivedAsync
函數的輸出是 Signal<T>
。因此,在 HTML 範本中顯示資料之前,我不需要使用 @if
來測試 undefined
。
@for (p of pokemons(); track p.id) {
<app-pokemon-container [pokemon]="p" pattern="derivedFrom" />
}
pokemons = derivedFrom([
this.createToObservable(this.id),
this.createToObservable(this.nextPokemon),
], { initialValue: [] as DisplayPokemon[] });
當我必須在示範中檢索多個 Pokemon 時,我會使用 derivedFrom
來獲取結果。如果 Observable
沒有立即發出值(例如:使用 startWith RxJS 運算子),則函數會拋出錯誤。修復方法是在函數的第二個參數中包含 initialValue
選項。 derivedFrom
的結果是 DisplayedPokemon
陣列的 Signal
。 範本中的 @for
將每個 pokemon 分配給 AppPokemonContainerComponent
元件的 signal input。
這些是我觀察到的所有可以使用 HttpClient
檢索資料並將結果儲存在 signal
中的模式。 我的首選是derivedAsync
,因為
AsynPipe
來解析 HTML 範本中的 Observable
requireSync
選項來發出同步數據鐵人賽的第 32 天到此結束。